package ar.com.arkios.kfcon4d.dao;

import ar.com.arkios.kfcon4d.Connection4D;
import ar.com.arkios.kfcon4d.exception.Open4DException;
import ar.com.arkios.kfcon4d.util.CuatroDUtils;
import ar.com.arkios.kfcon4d.util.EstructuraUtil;
import ar.com.arkios.j4d.opConstants;
import ar.com.arkios.j4d.opData;
import ar.com.arkios.j4d.opDataArray;
import ar.com.arkios.j4d.opException;
import ar.com.arkios.j4d.opFieldArray;
import ar.com.arkios.j4d.opProcess;
import ar.com.arkios.j4d.opSearch;
import ar.com.arkios.j4d.opSearchArray;
import ar.com.arkios.j4d.opSelection;
import ar.com.arkios.j4d.opTable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;

public abstract class GenericOperDAO<T extends Comparable> {
    private static final Logger miLogger = Logger.getLogger(GenericOperDAO.class);
    
    private String miNombre;
    /** Conección a 4D */
    protected Connection4D miConexion;
    
    /** 
     * Proceso a través del cual se ejecutan las consultas a la base de datos 
     * de 4D.
     */
    protected opProcess miProceso;
    
    private opTable miTablaEntidad;
    
    /**
     * Cantidad de registros a modificar o dar de alta en cada mini transaccion de 4D
     * Para no colgar totalmente el servidor
     */
    private int miCantRegsPorBloque = 150;
    
    /**
     * Cantidad de milisegundos a dormir entre cada mini transaccion, para que el
     * server 4D respire y le de bola a los demás clientes.
     */
    private int misMillisegADomir = 450;


    private opFieldArray miEntidadFields;
    private opFieldArray miEntidadPKFields;
    private opFieldArray miEntidadModFields;

    public GenericOperDAO() {
        miConexion = Connection4D.getInstance();
        miProceso = miConexion.getMyProcess();
        miTablaEntidad = miConexion.getTable(getEntidadTableName());
        miEntidadFields = EstructuraUtil.getFieldArrayFor(miTablaEntidad, getEntidadFieldsNames());
        miEntidadPKFields = EstructuraUtil.getFieldArrayFor(miTablaEntidad, getEntidadPKFieldsNames());
        miEntidadModFields = EstructuraUtil.getFieldArrayFor(miTablaEntidad, getEntidadModFieldsNames());
        
        this.miNombre = this.getClass().getName();
        this.miNombre = this.miNombre.substring(this.miNombre.lastIndexOf(".")+1);
    }

    public void saveAll(List<T> listaModelo) throws opException {
        try {
            // Obtengo la lista de códigos de los registros existentes en la base.);
            miLogger.debug("Inicio saveAll "+miNombre+" .Lista recibida con "+listaModelo.size()+" elementos");
            Set<String> claves = getListaCodigos();
            Set<String> clavesModificar = new HashSet<String>();

            // Divido la lista modelo en dos, los de ALTA y los de MODIFICACION.
            ArrayList<T> listaAlta = new ArrayList<T>();
            ArrayList<T> listaModificacion = new ArrayList<T>();

            // Separo la lista del modelo a persistir en dos: los que se tienen
            // que crear y los que se tienen que modificar.
            for (T elem : listaModelo) {
                String value = BeanUtils.getProperty(elem, getPKProperty());
                
                if (claves.contains(value)) {
                    listaModificacion.add(elem);
                    clavesModificar.add(value);
                } else {
                    listaAlta.add(elem);
                }
            }
        
            List<String> clavesViejas = new ArrayList<String>(CollectionUtils.subtract(claves, clavesModificar));
            miLogger.debug("SaveAll "+miNombre+": listas preparadas. Mod: "+listaModificacion.size()+" - Alta: "+listaAlta.size()+" - Baja: "+clavesViejas.size());
            int aProcesar;
            int desde;
            int hasta;
            int faltan;
            if (listaModificacion.size() > 0) {
                Collections.sort(listaModificacion);
                miLogger.debug("Update: listas armadas. Cantidad a modificar: "+listaModificacion.size());
                opSelection seleccion = null;
                //----------------------------------------------------------------------------------------------------
                desde = 0;
                faltan = listaModificacion.size();
                
                while (faltan > 0) {
                    aProcesar = Math.min(faltan, miCantRegsPorBloque);
                    hasta = desde + aProcesar;
                    miLogger.debug("modificación de "+miNombre+" - desde: "+desde+" - hasta: "+hasta);
                    opDataArray[] lista4DModificacion = mapListEntidadModTo4D(listaModificacion.subList(desde, hasta));                   
                    boolean todoOk;
                    
                    try {
                        miProceso.StartTransaction();
                        opDataArray[] lista4DModificacionPK = convert4DListEntidadToPK(lista4DModificacion);
                        seleccion = CuatroDUtils.buscarXOperlista(miProceso, lista4DModificacionPK, miEntidadPKFields);
                        // ahora en 4D esta la seleccion de registros a modificar
                        todoOk = (seleccion.mRecordsInSelection == aProcesar);
                    } catch (opException ex) {
                        miLogger.error("Error 4D buscando operativos a modificar: "+ex.GetErrorMsg());
                        todoOk = false;
                    } 
                    // Ordeno la seleccion ascendentemente y hago arrayToSeleccion
                    if (todoOk) {
                        CuatroDUtils.ordenar(miProceso, miEntidadPKFields, opConstants.GREATER_THAN);
                        miProceso.ArrayToSelection(lista4DModificacion, miEntidadModFields);
                        miProceso.ValidateTransaction();
                    } else {
                        miProceso.CancelTransaction();
                        miLogger.error("Cantidades encontradas y buscada no coincidentes.");
                        miLogger.error("Buscados: "+aProcesar);
                        miLogger.error("Encontrados: "+seleccion.mRecordsInSelection);
                    }

                    Thread.sleep(misMillisegADomir);
                    
                    desde += aProcesar;
                    faltan -= aProcesar;
                }
                //----------------------------------------------------------------------------------------------------
            }
            // Impacto las altas de los registros en ALTA.
            if (listaAlta.size() > 0) {
                miLogger.debug("Inicio alta de "+listaAlta.size()+" registros de "+miNombre);
                //----------------------------------------------------------------------------------------------------  
                desde = 0;
                faltan = listaAlta.size();

                while (faltan > 0) {
                    aProcesar = Math.min(faltan, miCantRegsPorBloque);
                    hasta = desde + aProcesar;
                    miLogger.debug("alta de "+miNombre+" - desde: "+desde+" - hasta: "+hasta);
                    miProceso.StartTransaction();
                
                    opDataArray[] lista4DAlta = mapListEntidadTo4D(listaAlta.subList(desde, hasta));
                    CuatroDUtils.alta(miProceso, lista4DAlta, miEntidadFields);
                    miProceso.ValidateTransaction();

                    Thread.sleep(misMillisegADomir);
                    
                    desde += aProcesar;
                    faltan -= aProcesar;
                }
                //----------------------------------------------------------------------------------------------------
                miLogger.debug("Alta finalizada");                
            }
            // Por último desactivo lo viejo
            if (clavesViejas.size() > 0) {
                miLogger.debug("Inicio desactivación de "+clavesViejas.size()+" registros");
                Collections.sort(clavesViejas);
                opDataArray[] clavesViejas4d = null;

                opFieldArray campos = new opFieldArray(1);
                campos.mFieldArray[0] = EstructuraUtil.findField(miEntidadFields, getActiveFieldName());
                campos.mTargetTable = campos.mFieldArray[0].mTableNumber;
                
                opDataArray[] datosModifViejos = new opDataArray[1];
                datosModifViejos[0] = new opDataArray(0);
                opData data = new opData(opConstants.BOOLEAN, false);
//----------------------------------------------------------------------------------------------------
                // Suponemos que los DAOs de operativo solo tiene PKs simples
                // Armo el opDataArray[] que contiene las claves viejas para 
                // armar la selection de lado de 4D.
                desde = 0;
                faltan = clavesViejas.size();
                
                while (faltan > 0) {
                    aProcesar = Math.min(faltan, miCantRegsPorBloque);
                    hasta = desde + aProcesar;
                    miLogger.debug("desactivación de "+miNombre+" - desde: "+desde+" - hasta: "+hasta);
                    boolean todoOk = true;
                    
                    if (getMiEntidadPKFields().mFieldArray[0].mFieldType == opConstants.ALPHANUMERIC) {
                        clavesViejas4d = EstructuraUtil.getStringPKDataArray(clavesViejas.subList(desde, hasta));
                    } else if (getMiEntidadPKFields().mFieldArray[0].mFieldType == opConstants.LONGINTEGER) {
                        clavesViejas4d = EstructuraUtil.getIntegerPKDataArray(clavesViejas.subList(desde, hasta));
                    } else {
                        String msg = "La clave primaria indicada en el DAO no es soportada para el proceso de migración";
                        miLogger.error(msg);
                        todoOk = false;
                    }
                    
                    if (todoOk) {
                        miProceso.StartTransaction();
                        opSelection seleccion = CuatroDUtils.buscarXOperlista(miProceso, clavesViejas4d, miEntidadPKFields);

                        if (seleccion.mRecordsInSelection == aProcesar) {
                            if (aProcesar != datosModifViejos[0].mSize){
                                datosModifViejos[0] = new opDataArray(aProcesar);

                                for (opData d : datosModifViejos[0].mDataArray)
                                    d = data;
                            }
                            miProceso.ArrayToSelection(datosModifViejos, campos);
                            miProceso.ValidateTransaction();
                        } else {
                            miProceso.CancelTransaction();
                            miLogger.error("Cantidades encontradas y buscada no coincidentes.");
                            miLogger.error("Buscados: "+aProcesar);
                            miLogger.error("Encontrados: "+seleccion.mRecordsInSelection);
                        }                    

                        Thread.sleep(misMillisegADomir);
                    }
                    desde += aProcesar;
                    faltan -= aProcesar;
                }
                //----------------------------------------------------------------------------------------------------
                miLogger.debug("Modificación claves viejas finalizada");
            }
        } catch (opException ex) {
            miLogger.error("Error durante la actualización de "+this.miNombre);
            miLogger.error(ex.GetErrorMsg());
            miProceso.CancelTransaction();
        } catch (Exception e) {
            miLogger.error("No se pudo obtener el código de una de las entidades del modelo.");
            miLogger.error(e);
            miProceso.CancelTransaction();
        }
    }

    // TODO: hacer esto de manera generica, para que los códigos puedan ser de
    // cualquier tipo. QUEDA PENDIENTE SI TIENE PK COMPUESTA COMO DEVUELVO EL
    // EL ELEMENTO DE LA COLECCIÓN
    private Set<String> getListaCodigos() {
        Set<String> result = new HashSet<String>();
        try {
            opSearchArray criterioSAP = new opSearchArray(1);
            short elNroCampoEntidad = EstructuraUtil.getFieldNumber(miTablaEntidad, "AMano");
            opData dataSAP = new opData(opConstants.BOOLEAN, false);
            criterioSAP.mSearchArray[0] = new opSearch(opConstants.NONE, miTablaEntidad.mTableNumber, elNroCampoEntidad, opConstants.EQUAL, dataSAP);
            opSelection selection = miProceso.Search(criterioSAP);
            
            opDataArray[] dataArrayPK = new opDataArray[miEntidadPKFields.mSize];
            for (int i = 0; i < dataArrayPK.length; i++) {
                dataArrayPK[i] = new opDataArray(selection.mRecordsInSelection);
            }
            
            // TODO: cambiar cuando se empeice  a trabajar con claves compuestas.
            miProceso.SelectionToArray(dataArrayPK, miEntidadPKFields);
            
            // Por ahora hago un for hasta la cantidad de registros. Cuando tenga
            // clave compuesta creo que voy a tener un FOR mas externo, para las claves.
            // y concatenar claves con separador especial ????????????
            for (int i = 0; i < dataArrayPK[0].mSize; i++) {
                //como 4D opProcesos.SelectionToArray NO LLENA opData.mType !!! lo tenemos que hacer nosotros a mano
                dataArrayPK[0].mDataArray[i].mType = (short) miEntidadPKFields.mFieldArray[0].mFieldType;
                String clave = getStringDataValue(dataArrayPK[0].mDataArray[i]);
                result.add(clave);
            }
            
        } catch (opException e) {
            miLogger.error("No se pudo acceder a los codigos de "+miNombre);
            miLogger.error(e);
            throw new Open4DException(e);
        } catch (Exception e) {
            miLogger.error("Ocurrió un error inesperado en getListaCodigos");
            miLogger.error(e);
            throw e;
        } finally {
            return result;
        }
    }
    
    private String getStringDataValue(opData elDato) {
        switch (elDato.mType) {
            case opConstants.ALPHANUMERIC:
                return elDato.mString;
            case opConstants.LONGINTEGER:
                return String.format("%1$07d", elDato.mLongInteger);
            default:
                throw new Open4DException("Tipo de PK no contemplado.");
        }
    }
    
    protected abstract String getEntidadTableName();
    
    protected String getEntidadAtributoTableName(){
        // No hace nada excepto que lo sobreescriban.
        return null;
    }
    
    protected abstract String getPKProperty();
    
    protected abstract String getActiveFieldName();
    
    /**
     * Data un lista de entidades la convierte a modo lista para 4D
     * 
     * @param laLista lista de entidades a convertir
     * @return lista 4D equivalente
     */
    protected abstract opDataArray[] mapListEntidadTo4D (List<T> laLista);
    
    protected abstract opDataArray[] mapListEntidadModTo4D (List<T> laLista);
    
    /**
     * Data una lista de entidades en formato 4D arma una lista equivalente tambien en formato 4D
     * pero con solamente las PK
     * 
     * @param laLista con las entidaes
     * @return lista de solo las PK
     */
    protected opDataArray[] convert4DListEntidadToPK (opDataArray[] laLista) {
        opDataArray[] lasPK = new opDataArray[getEntidadPKFieldsNames().length];
        for (int i = 0; i < getEntidadPKFieldsNames().length; i++) {
            int j;
            for (j = 0; j < getEntidadModFieldsNames().length; j++) {
                if (getEntidadPKFieldsNames()[i].equals(getEntidadModFieldsNames()[j]))
                    break;
            }
            if (j < getEntidadModFieldsNames().length) {
                lasPK[i] = laLista[j];
            } else {
                miLogger.error("No se encontro la columna PK '" + getEntidadPKFieldsNames()[i] + "' en las columnas indicadas en el DAO.");
                throw new Open4DException("Error en el mapeo de columnas y columnas PK del DAO");
            }
        }
        return lasPK;
    }

    /**
     * Devuelve los nombres de las columnas de la entidad a ser tenidos en cuenta.
     */
    protected abstract String[] getEntidadFieldsNames();
    
    /**
     * Devuelve los nombres de las columnas de la PK de la entidad.
     */
    protected abstract String[] getEntidadPKFieldsNames();
    
    protected abstract String[] getEntidadModFieldsNames();

    protected opFieldArray getMiEntidadFields() {
        return miEntidadFields;
    }

    protected opFieldArray getMiEntidadPKFields() {
        return miEntidadPKFields;
    }

    protected opFieldArray getMiEntidadModFields() {
        return miEntidadModFields;
    }

    protected opTable getMiTablaEntidad() {
        return miTablaEntidad;
    }
    
    public int getCantRegsPorBloque() {
        return miCantRegsPorBloque;
    }

    public void setCantRegsPorBloque(int cantRegsPorBloque) {
        this.miCantRegsPorBloque = cantRegsPorBloque;
    }

    public int getMillisegADomir() {
        return misMillisegADomir;
    }

    public void setMillisegADomir(int millisegADomir) {
        this.misMillisegADomir = millisegADomir;
    }
        
}